iT邦幫忙

2026 iThome 鐵人賽

DAY 28
0
自我挑戰組

一鍵完成六套開源防禦系統整合系列 第 28 篇

od-bridge:事件從這裡出去,決策從這裡回來

  • 分享至 

  • xImage
  •  

搭配元件篇談的是別人寫的八個系統。安裝包裡只有一支程式是自己寫的,就是 od-bridge:它坐在主機 B 上,一邊把 Vector 篩過的事件簽章後送往主機 A,一邊每 5 秒去主機 A 拉回封鎖決策,落地到這台主機的防火牆、CrowdSec 與一份給外部防火牆抓的清單。總覽第 3 篇用一節帶過它,這一篇把它拆開來講:它的兩個迴圈、四個執行點、它在記憶體與磁碟各留了什麼、主機 A 不在的時候它怎麼辦,以及讀者排錯時最常要看的它的紀錄。 主機與位址沿用防禦節點那一套:主機 A(192.168.0.111)是 BeakPlatform,主機 B(192.168.0.112)是防禦節點。

先講結論。 od-bridge 是主機 B 與主機 A 之間唯一的一條線,兩個方向都是它。往主機 A 的方向,它是 Vector 的下游:收一筆、簽一筆、轉一筆,自己不排隊、不重試,送不出去就把失敗原樣交回 Vector,由 Vector 決定要不要重送。往主機 B 的方向,它是主機 A 的執行端:每 5 秒問一次「有沒有待執行的決策」,拿到就逐一落地,再把每個執行點的成敗回報回去。它不判斷任何事情:要不要送、送哪些,Vector 決定;要不要封、封多久,主機 A 的流程決定。它做的是「把兩邊接起來」,約一千行 Python,只用一個第三方函式庫。

另外有兩件事值得先知道。第一,它幾乎不留東西:轉送紀錄、決策紀錄、計數器全在記憶體裡,重啟歸零;磁碟上只有 CrowdSec 的機器憑證與 EDL 清單。第二,它拿到的權限不小:它跑在主機網路上,帶著改防火牆的能力,能在這台主機的 kernel 裡加減封鎖項目。這兩件事合起來決定了第六節「它不做的事」與第五節「主機 A 不通的時候」會看到的行為。

一、它是什麼

od-bridge 的 od 是 Open Defense,主機 A 上負責資安案件的那個模組的名字;bridge 就是橋。它是安裝包裡唯一自己寫的程式(Apache 2.0),Python,六個檔案加四個執行點模組,連空行與註解共 1,209 行,扣掉之後約 1,000 行。唯一的第三方相依是 aiohttp,同時當 HTTP 伺服器(收 Vector、給管理者看狀態頁)與 HTTP 用戶端(送主機 A、打 CrowdSec)。它不是一個「產品」,沒有版本號,隨安裝包一起更新。

它在本安裝包裡的樣子:

項目 內容
是否安裝 一定會裝,沒有關閉選項。沒有它,主機 A 一筆事件都收不到、一筆決策都落不了地
怎麼跑 容器,但用主機網路(不在其他容器的那個 docker 網段裡),並帶 NET_ADMIN 能力。理由只有一個:它要用 nft 指令改主機 kernel 的防火牆,指令必須在主機的網路命名空間裡下。副作用是它連其他容器都得走 127.0.0.1(CrowdSec 的 8081 埠),而 Vector 要連它得走 host.docker.internal
映像檔 安裝時從 od-bridge/Dockerfile 現場建:python:3.12-slim 加 nftables、iproute2、curl 三個套件,實測 235 MB。容器內以 root 執行
監聽 0.0.0.0:8500。路由只有八條:POST /events(Vector 送事件進來,要 Bearer token)、GET /health、/stats(也是首頁)、/forwards、/decisions、/edl、/edl/allow、/state/nft。除了 /events,其餘都沒有登入
誰能連它 主機 B 的防火牆只放行主機 A 與 --admin-ips;本機與 docker 網段內部(Vector)不受限。外部防火牆要抓 /edl 也得在那份名單裡
它的設定 全部從 .env 讀,沒有自己的設定檔。安裝時 --pair 貼進來的開通字串(ODN1. 開頭)解開後就是主機 A 的網址、事件受理金鑰(INTAKE_KEY_ID/INTAKE_SECRET_B64)與執行帳號(SA_ID/SA_SECRET);其餘有預設值,第三節逐項講
它自己的資料 主機目錄 od-bridge/state/ 掛進容器的 /state,裡面只有兩樣:CrowdSec 的機器憑證(安裝腳本替它註冊後寫入,0600)與 EDL 的清單目錄。其餘全在記憶體
資源 實測一台節點閒置時約佔 24 MB 記憶體,是整台主機上最省的一個

二、它接在哪裡

https://ithelp.ithome.com.tw/upload/images/20261012/201842615805stYH1J.png
(圖01:od-bridge 的位置。黑色實線是主機 B 內部的呼叫,藍色虛線是事件往主機 A 的方向,紅色點線是決策從主機 A 回來的方向,灰色細線是管理者與外部防火牆的讀取。)

圖上有三件事要看。

  • 兩個方向是兩個獨立的迴圈,互不等待。 左邊那條是 Vector 推過來的,od-bridge 被動接;下面那條是它自己主動去主機 A 拉的。事件送不送得出去,不影響決策拉不拉得回來,反過來也一樣。所以會出現「主機 A 收不到事件、但封鎖照常落地」或相反的組合,第五節講。
  • 右邊三個落地點,它只負責「寫進去」。 nftables 那一筆是 kernel 自己在守,CrowdSec 那一筆要有 bouncer 才會擋人,EDL 那一份要有外部防火牆來抓才有作用。od-bridge 寫完就回報成功,至於「有沒有真的擋到人」,它不知道也不查。
  • 主機 A 的兩個端點用的是兩套身分。 送事件用事件受理金鑰,每筆 HMAC 簽章;拉決策用執行帳號,先換一張 15 分鐘的 JWT。兩套各自有自己的限流與鎖定規則,壞起來的樣子也不同,第七節的紀錄表會分開列。

三、設定值:全在 .env

od-bridge 沒有設定檔,啟動時讀環境變數,容器用 env_file: .env 把整份 .env 餵給它。下面是它會讀的每一個,分成「開通字串帶進來的」與「安裝腳本給預設值的」兩組:

變數 來源/預設 意思
BEAK_BASE_URL 開通字串 主機 A 的網址,例如 http://192.168.0.111:8000/beakplatform。INTAKE_URL、SA_LOGIN_URL、DECISIONS_URL 三個由它衍生,安裝腳本寫進 .env
INTAKE_KEY_ID、INTAKE_SECRET_B64 開通字串 事件受理金鑰。金鑰的 secret 是 base64url 編碼,od-bridge 啟動時解碼成 raw bytes 再拿來簽章。這把金鑰在主機 A 上有「允許的事件來源」清單(出廠 coraza、suricata、crowdsec、vector),來源不在清單上的事件主機 A 回 403
SA_ID、SA_SECRET 開通字串 執行帳號(service account)。兩者留空,executor 迴圈整個不啟動,紀錄會印一行 executor DISABLED,節點變成「只送事件、不執行決策」。安裝腳本在這種情況下會警告一次
MY_ENFORCEMENT_POINTS nftables,crowdsec,edl 這台節點負責哪些執行點。拉回來的決策,它只處理「決策上標的執行點」與這份清單有交集的;完全沒交集的決策,它連 picked_up 都不回報,留在主機 A 等別的執行端。cloudflare 刻意不在預設值裡,第四節
APPLIED_BY od-bridge@<主機名> 回報決策時的署名,主機 A 的決策頁「執行者」欄顯示的就是它。多台節點時靠這個分辨是誰落地的
POLL_INTERVAL 5 executor 每幾秒去主機 A 拉一次。主機 A 對每個執行帳號限每分鐘 60 次,5 秒一次是 12 次,離上限還遠;改到 1 秒就會撞到
BRIDGE_INGEST_TOKEN 安裝時隨機產生 Vector 送 /events 要帶的 Bearer token,Vector 的設定檔讀的是同一個變數。空值時 /events 整個關閉,每一筆都 401,紀錄印 BRIDGE_INGEST_TOKEN not set
INGEST_LISTEN 0.0.0.0:8500 監聽位址。因為是主機網路,這個埠直接開在主機上,由主機防火牆管來源
CROWDSEC_LAPI_URL http://127.0.0.1:8081 CrowdSec 的本機 API。CrowdSec 容器把 8080 發布到主機的 127.0.0.1:8081,只開本機,就是為了給走主機網路的 od-bridge 用
EDL_DIR、EDL_PRUNE_INTERVAL、EDL_HEADER /state/edl、60、0 EDL 清單放哪、每幾秒剔除一次過期項目、要不要在檔頭加註解。註解預設關,理由在第四節

開通字串本身只是一段 base64url 編碼的 JSON,裡面是上面前三列的五個值加企業網域,由主機 A 的 od_node_pairing.py 產生。它不是什麼安全機制,只是把五個值打包成一行方便貼;拿到字串的人就等於拿到這台節點對主機 A 的兩套身分,所以示範憑證檔才設成 root 0600。

四、兩個迴圈與四個執行點

ingest:收一筆、簽一筆、轉一筆

Vector 的 bridge_intake 出口把每一筆事件 POST 到 :8500/events。od-bridge 做的事按順序是:

  1. 比對 Authorization: Bearer 與 BRIDGE_INGEST_TOKEN,不符 401。比對用的是常數時間的函式,而且刻意以 bytes 比對:早期版本用字串比對,送非 ASCII 的標頭會讓它拋例外變 500 而不是 401。
  2. body 必須是 JSON,且至少有 correlation_id 與 source_system 兩個欄位,否則 400。它只看這兩個,其餘欄位對不對是主機 A 的事。
  3. 取現在的 Unix 秒數當時間戳,對「時間戳、換行、原始 body」算 HMAC-SHA256,放進三個標頭:X-OD-Key-Id、X-OD-Timestamp、X-OD-Signature(值是 sha256= 加十六進位)。body 一個 byte 都不改地轉送,連 Vector 送來的 JSON 空白都保留,因為簽的是原始 bytes。
  4. 等主機 A 回應,最多 10 秒。回應碼與 body 原樣交回給 Vector,只有 5xx 一律改成 502;連不上(逾時、拒絕連線、DNS 失敗)也回 502,body 是 upstream_unreachable 加原因。
  5. 在記憶體記一筆:時間、來源、主機 A 的回應碼(連不上記 0)、correlation_id,就是 /forwards 頁的一列;同時紀錄印一行 forwarded cid=… src=… status=…。

整個過程是同步的:Vector 的那一筆要等到主機 A 回應了才拿到結果。這是刻意的,od-bridge 沒有自己的佇列、沒有重試、沒有磁碟緩衝,「送不出去怎麼辦」整個交給 Vector。Vector 篇第四節講的記憶體排隊、隊首重試、重啟遺失,全是這個設計的結果。od-bridge 自己只是一個會簽章的轉接頭。

主機 A 那一端怎麼驗:用 X-OD-Key-Id 找金鑰,用金鑰的 secret 重算一次簽章比對,時間戳與主機 A 的時鐘差超過 5 分鐘一律拒絕(防重放),三者任一不對都回同一個 401 auth_failed,不說是哪一個。所以「主機 B 的時鐘慢了六分鐘」在 od-bridge 的紀錄裡長得和「金鑰貼錯」一模一樣,排錯時先對兩台的時間。通過認證之後,主機 A 依序查金鑰的來源範圍(403 scope_denied)、事件格式(400 validation_error)、企業有沒有對應的事件路由(422 no_mapping),都過了才建案,回 200 並在 body 標示是不是重複的 correlation_id。主機 A 另有每把金鑰每分鐘 100 筆、每小時 5,000 筆的限流,超過回 429。

executor:每 5 秒問一次,拿到就落地

第二個迴圈不等任何人,自己定時跑。一輪做的事:

步驟 內容
換 token 手上的 JWT 還剩 60 秒以上就直接用;否則 POST sa/login 用 SA_ID/SA_SECRET 換一張,效期由主機 A 決定(預設 900 秒)。所以正常運作時紀錄每 14 分鐘出現一行 SA login ok, expires in 900s,這是健康的樣子,不是一直在重登。主機 A 對登入端點限每來源 IP 每分鐘 10 次,密碼錯 5 次鎖 15 分鐘
拉決策 GET decisions?status=pending&limit=100。主機 A 只回這個執行帳號所屬企業的、且執行點與帳號被允許的執行點有交集的決策;od-bridge 再用 MY_ENFORCEMENT_POINTS 篩一次。一筆決策長這樣:action(block/unblock/allow)、target_type(ip/cidr/country/asn…)、target_value、enforcement_points、ttl_seconds、reason、case_secure_code
宣告拾起 先 PATCH 一次 status=picked_up。主機 A 這一步用樂觀鎖(只有還是 pending 的才改得動),兩台節點同時拉到同一筆時只有一台會成功,另一台拿到 409。但 od-bridge 收到 409 只記一行警告,照樣往下落地;這在單一節點的部署裡沒有差別,多節點時要知道鎖只擋了「回報」沒擋「執行」
逐點落地 依決策上的執行點順序,一個一個呼叫對應的模組(下一段),每個回 {"ok": true/false, …}。一個執行點拋例外不影響下一個。同一輪拉到多筆決策時,多筆之間是併行的,單一決策的多個執行點是依序的
回報 全部 ok → applied;有成有敗 → partial;全敗 → failed(附錯誤摘要)。PATCH 的 body 帶 application_result,每個執行點的回傳原樣放進去,主機 A 的決策頁點開就看得到。所以「狀態是 applied」和「nftables 那格寫了什麼」是兩件事,前者只是後者的摘要。執行端只能寫這四種狀態;expired 與 revoked 是主機 A 自己的排程與管理員才能寫,執行端送了會被 403
出錯時 GET 回 401:先當 token 壞了,作廢重換;連續 401 則每次多等 60 秒、最多 10 分鐘,避免把執行帳號的登入配額燒光。GET 回 429:照 Retry-After 等。其他錯誤(主機 A 不通、502):從 1 秒開始指數退避、最長 60 秒一次,恢復後歸零。登入被 429:等 90 秒

實測的時序(2026 年 10 月 4 日在測試環境走的一整圈):資安人員在主機 A 的案件上按「封鎖」,決策寫入;od-bridge 在下一次輪詢拿到,三個執行點依序落地,PATCH applied,從決策寫入到 applied 只有幾秒。主機 A 決策頁上那筆的 application_result 長這樣(三個執行點各自的回報):

{"nftables": {"ok": true, "set": "blocklist", "element": "203.0.113.42 timeout 3600s"},
 "crowdsec": {"ok": true, "status": 201, "body": "[\"142\"]"},
 "edl":      {"ok": true, "file": "/state/edl/blocklist.txt", "block_entries": 1, "allow_entries": 0}}

接著管理員撤銷:主機 A 把原決策標成 revoked,並新產生一筆 unblock 決策,執行點與原決策相同;od-bridge 下一輪拉到,三處各自移除,回報 applied。TTL 到期也是同一套:主機 A 每分鐘的排程把過期的 applied 決策標成 expired、產生一筆 unblock。od-bridge 從不自己判斷「到期了」,解封永遠是主機 A 下的一筆新決策;它唯一自己做的到期處理是 EDL 清單的剔除(下一段)。

四個執行點

執行點 它做什麼 要知道的事
nftables 對主機 kernel 下 nft add element inet secstack blocklist { 203.0.113.42 timeout 3600s };IPv6 進 blocklist6;unblock 是 delete element。只認 ip 與 cidr,allow 不支援 那張表、那兩個 set 與「來源命中就 drop」的規則,是安裝腳本的 nftables.sh 建的,od-bridge 只加減元素。timeout 是交給 kernel 的:主機 A 與 od-bridge 都掛了,kernel 仍會在 TTL 到期時自己剔除。刪除時「元素不存在」算成功(可能 kernel 已經先到期了)。同一張表另有 allowlist(主機 A 與管理來源)排在 drop 之前,所以就算決策寫錯目標,管理者也不會把自己鎖在外面。它只擋「以該 IP 為封包來源、直連主機 B」的流量:經 Cloudflare tunnel 進來的請求,封包來源是 Cloudflare,這一條擋不到,要靠 WAF 層或 Cloudflare 自己(第 5 篇)
crowdsec 用安裝時註冊的機器身分登入 CrowdSec 的本機 API,block 是 POST 一筆 alert、內含一筆 type=ban、origin=od-bridge、scenario=od-bridge 的決策,期限等於 TTL(沒有 TTL 就一年);unblock 是 DELETE 符合「同範圍、同目標、scenario=od-bridge」的決策。支援 ip/cidr/country/asn 四種目標,是四個執行點裡最廣的 機器憑證在 state/crowdsec_machine.json,由安裝腳本 cscli machines add od-bridge 產生;檔案不在,這個執行點回 no_machine_creds、整筆決策變 partial。CrowdSec 的 API 對登入的 User-Agent 格式很挑(必須是 名稱/版本),2026 年 10 月 4 日之前的版本因此一律 401,CrowdSec 篇講過。寫進去的 ban 本機沒有 bouncer 在讀,所以它在這台主機上不擋任何人;它的價值是「別台接了 bouncer 的主機能拿到同一份清單」。手動 cscli decisions list 看得到,Source 欄是 od-bridge
edl 維護 state/edl/state.json(每筆目標帶自己的 expires_at),每次異動與每 60 秒重新渲染整份 blocklist.txt/allowlist.txt,由 /edl 與 /edl/allow 以純文字提供。block 進黑名單、allow 進白名單、unblock 從兩份都移除。只認 ip 與 cidr 它是四個裡唯一支援 allow 的,所以平台「放行」決策的落地點就是它(總覽第三節第 4 條)。為什麼是「重寫整份」而不是「追加一行」:EDL 是防火牆定期抓的純文字,格式裡沒有每筆的期限,抓到什麼就是當下的政策,追加式的檔案永遠刪不掉過期項目。輸出預設沒有註解、沒有標頭,因為只有部分設備接受「位址 空格 註解」的寫法;EDL_HEADER=1 可以加,給人用 curl 看的時候才開。空清單回的是空檔案而不是 404:PAN-OS 抓到 404 會沿用上一次的內容,等於把舊黑名單釘住。寫檔是先寫暫存再 rename,防火牆不會抓到寫一半的檔。啟動時先渲染一次,所以重啟後檔案一定在。注意主機 A 自己也能產一份 EDL(讀者文件的「防火牆黑名單」那一頁),那一份是從主機 A 的決策表直接算的,不經過 od-bridge;兩份內容應該一樣,來源不同
cloudflare 預留。任何決策一律回 not_configured 指的是「把封鎖推到 Cloudflare 的 IP 清單」這個執行點,不是 Cloudflare tunnel 入口(那是 cloudflared 篇,完整可用)。主機 A 的決策節點在寫決策前會依每個執行點「支援哪些動作」過濾,cloudflare 支援的動作是空集合,所以正常情況下不會有決策標到它;執行帳號的允許清單出廠含它,是替未來實作留的位子

五、主機 A 不通的時候

兩個迴圈分開看。

ingest 這一邊:每筆轉送都連不上,od-bridge 立刻回 502 給 Vector,紀錄每筆印一行 forward failed 加 Cannot connect to host,/stats 的 unreachable 計數跳、/forwards 頁同一個 correlation_id 每 20 到 30 秒出現一列、狀態 0。它自己什麼都不留,排隊的是 Vector。這一段 Vector 篇第四節整節在講,不重複;要記得的只有「/forwards 頁同一筆反覆出現」是 Vector 在重試隊首,不是 od-bridge 壞了。

executor 這一邊:GET 失敗就退避,1、2、4、…、60 秒一次,紀錄每次一行 GET decisions 502 或 executor loop error。主機 A 回來之後,下一次輪詢就恢復 5 秒一次,期間累積在主機 A 的 pending 決策一次拉完(一次最多 100 筆)。沒有任何決策會因為主機 A 停機而遺失,它們本來就在主機 A 的資料表裡等。

反過來,od-bridge 自己重啟或停掉,會丟的是:/stats、/forwards、/decisions 三頁的內容(記憶體)、手上的 JWT(重啟後重登一次)。不會丟的是:kernel 裡的 blocklist(連剩餘的 timeout 都在,因為那是 kernel 的狀態,不是它的)、CrowdSec 裡的 ban、EDL 的清單與 state.json。所以「重啟 od-bridge」是安全的操作,封鎖不會因此失效或重複。安裝腳本的 --reconfigure 重建防火牆表時會先把 blocklist 的元素連同剩餘 timeout 記下來再補回去,也是同一個理由。

還有一種「不通」是認證層的:主機 A 活著,但事件一律 401。可能是開通字串貼錯、金鑰在主機 A 被停用,或兩台主機的時鐘差超過 5 分鐘。三者在 od-bridge 的紀錄裡長得一樣,都是 status=401;Vector 對 401 不重試,所以這些事件在主機 A 那邊就沒了(ClickHouse 仍有)。這是整條鏈裡少數「靜默遺失」的點,第七節的表有列。

六、它不做的事

限制 說明
不排隊、不重試事件 第四節。轉送失敗原樣交回 Vector,由 Vector 重試;它自己沒有佇列也沒有磁碟緩衝
不判斷到期 解封永遠是主機 A 下的新決策。它唯一自己做的到期處理是 EDL 清單的剔除,因為那份檔案格式裡沒有期限欄位、只能由產檔的一方負責
不確認「有沒有真的擋到」 寫進 nft set、寫進 CrowdSec、寫進檔案,三者都是「寫入成功」就回 ok。CrowdSec 沒有 bouncer、EDL 沒有防火牆來抓,決策照樣 applied
狀態頁沒有登入 /stats、/forwards、/decisions、/state/nft 能連就能看,裡面有攻擊者 IP、決策編號、案件編號。防線只有主機防火牆的來源名單。它是給內網值班的人看的,不是給 Internet 的
狀態頁不是稽核紀錄 記憶體裡最近 200 筆、頁面顯示 50 筆,重啟歸零。要稽核去主機 A 的決策表與 ClickHouse
樂觀鎖只擋回報 第四節。多台節點同時拉到同一筆,輸的那一台照樣落地。單一節點不受影響
Vector 到它之間沒有加密 主機內部的 HTTP,Bearer token 明文。不出主機 B
只處理有交集的決策 決策的執行點與 MY_ENFORCEMENT_POINTS 沒交集,它看都不看。只標 cloudflare 的決策會永遠 pending。正常情況下主機 A 不會產生這種決策,但手動改過 MY_ENFORCEMENT_POINTS 之後要記得這條
nftables 只擋直連流量 第四節。經 tunnel 進來的請求封包來源是 Cloudflare,這一條擋不到
容器內是 root,帶 NET_ADMIN 能改主機防火牆的程式必然如此。它的攻擊面是 8500 埠的八條路由,其中只有 /events 會改變什麼,而且要 token;其餘都是唯讀。但「能連到 8500 的人」這份名單要當成「能看到所有攻擊者 IP 的人」來管

把它停掉會怎樣:Vector 的 bridge_intake 出口連不上,回應是連線被拒而不是 502,Vector 一樣當失敗重試、隊首卡住,行為與主機 A 不通相同;主機 A 的新決策堆在 pending;已經在 kernel 裡的封鎖照常到期、CrowdSec 裡的 ban 照常到期、EDL 檔不再更新(外部防火牆抓到的是停掉當下那一份,過期的項目不會自己消失)。WAF、Suricata、ClickHouse、Grafana 全部不受影響。重新啟動後一切自己接上,不需要任何手動步驟。

七、怎麼確認它在做事

先看活著、設定對不對。第一個指令就是 install.sh --verify 裡「od-bridge /health」那一行;第二個看它啟動時印的三行,從這三行可以直接讀出它認為自己的角色是什麼:

# 主機 B
curl -s http://127.0.0.1:8500/health
{"ok": true}

cd /opt/integrated-waf
sudo docker compose logs --no-color od-bridge | grep ' main: \| ingest server'
od-bridge-1  | … INFO main: executor enabled, polling every 5.0s for points=nftables,crowdsec,edl
od-bridge-1  | … INFO main: edl enforcer enabled, dir=/state/edl prune every 60.0s
od-bridge-1  | … INFO ingest: ingest server :8500 → http://192.168.0.111:8000/beakplatform/api/open_defense/intake

看到 executor DISABLED (no SA credentials in env) 就是 SA_ID 沒帶進來,節點只會送事件。再看 /stats,四組數字各自回答一個問題:

# 主機 B(或 --admin-ips 內的瀏覽器開 http://192.168.0.112:8500/stats)
curl -s http://127.0.0.1:8500/stats | sed 's/<[^>]*>/ /g' | tr -s ' \n'
 Stats uptime 43692s · last executor iter +1s ago
 Intake (Vector → bridge → BP) 1 total 1 2xx 0 4xx 0 5xx 0 unreachable
 Executor (BP /decisions) 0 processed 0 applied 0 partial 0 failed
 SA token logins: 52 · 429: 0 · GET 401: 0 · current token valid for: 191s
看哪裡 意思
last executor iter +1s ago executor 迴圈剛跑過。超過 60 秒沒動,看紀錄最後一行是不是在退避
Intake 的 2xx 對 4xx/5xx/unreachable 送出去的事件主機 A 收了幾筆、拒了幾筆、連不上幾筆。4xx 多半是 401(認證)、403(來源範圍)、422(沒路由),到 /forwards 看是哪一種
Executor 的 applied/partial/failed 落地的決策各幾筆。partial 多半是 CrowdSec 那一格失敗(憑證檔不在或舊版 User-Agent 問題),到主機 A 決策頁看 application_result
SA token 的 logins 與 valid for 上面那台跑了 12 小時登入 52 次,就是每 14 分鐘一次,正常。429 或 GET 401 不是 0,執行帳號有問題

要看封鎖有沒有真的落在這台主機上,看 kernel 而不是看 od-bridge。/state/nft 是 od-bridge 替你跑 nft -j list set 的結果,與直接下指令等價:

# 主機 B
curl -s http://127.0.0.1:8500/state/nft
{"blocklist": {"203.0.113.42": 3412}, "blocklist6": {}, "allowlist": ["192.168.0.50", "192.168.0.111"]}
#                              ^ 剩餘秒數,kernel 自己在倒數

sudo nft list set inet secstack blocklist
sudo docker compose exec -T crowdsec cscli decisions list       # Source 欄是 od-bridge 的就是平台下發的
curl -s http://127.0.0.1:8500/edl                                 # 一行一個,沒有別的

要走一整圈,install.sh --test-event 丟一筆攻擊者 203.0.113.42 的測試事件進 Vector,幾秒後 /forwards 多一列 status=200、主機 A 多一張案件;到案件上按「封鎖」,幾秒後 od-bridge 的紀錄印 decision … action=block target=ip/203.0.113.42 points=[…],上面三個指令各多一筆;撤銷之後各少一筆。這條路徑在讀者文件的安裝章節有逐步寫,這裡只列它在 od-bridge 這一端的樣子。

最後是它的紀錄。正常運作時它話很少:每 14 分鐘一行登入、每筆轉送一行、每筆決策一行,其餘出現的都值得看:

紀錄裡出現 意思
forwarded cid=… src=… status=200 正常。主機 A 收了(body 會說是不是重複的)
forwarded … status=401 主機 A 不認這把金鑰:開通字串貼錯、金鑰被停用,或兩台時鐘差超過 5 分鐘。三者訊息相同,先 date -u 對時間。Vector 對 401 不重試,這些事件在主機 A 端就沒了
forwarded … src=crowdsec status=403 金鑰的「允許的事件來源」沒有這個來源。2026 年 10 月 5 日之前配對的節點會遇到,到主機 A 的 API Key 頁加一行 crowdsec
forwarded … status=422 主機 A 收了但建不了案:企業沒有這個來源的事件路由(no_mapping),或事件內容過不了業務規則。主機 A 的紀錄有細節
forwarded … status=429 撞到主機 A 每把金鑰每分鐘 100 筆的上限。Vector 會等一下重送。正常流量下 Vector 的全域封頂一分鐘才放 8 筆,看到這個通常是主機 A 剛恢復、Vector 在倒排隊的事件
forward failed 加 Cannot connect to host,/forwards 同一筆反覆出現 主機 A 不通。第五節。主機 A 回來自己好
unauthorized /events request from … Vector 帶的 token 不對:BRIDGE_INGEST_TOKEN 改過但只重啟了一邊。兩個容器都重啟。若來源 IP 不是 docker 網段,是別人在試這個埠
BRIDGE_INGEST_TOKEN not set; /events is closed .env 缺這個值,所有事件都進不來。重跑 --reconfigure 會補
executor DISABLED (no SA credentials in env) SA_ID/SA_SECRET 空。只送事件、不執行決策,主機 A 的決策會一直 pending
SA login error: SA login 401 執行帳號的 id 或 secret 錯,或錯 5 次後被鎖 15 分鐘(body 會寫 locked)。每次重試間隔會拉長到 60 秒
SA login error: SA login 500 主機 A 缺簽 JWT 用的密鑰(OD_SA_JWT_SECRET),舊版安裝腳本不會產生。在主機 A 跑一次 install.sh --update
GET 401 — invalidating token;接著 (Nx in a row) — backing off token 換了還是 401,多半是主機 A 換過簽 JWT 的密鑰或執行帳號被停用。退避會拉到 10 分鐘一次,修好之後最多等 10 分鐘自己恢復,等不及就重啟 od-bridge
GET decisions 502: <html> 主機 A 的 nginx 活著、後面的應用沒起來(重啟中)。幾秒一行,應用起來就停
decision <sc> action=block target=ip/… points=[…] 正常。拉到一筆並開始落地
enforcer crowdsec failed 或 crowdsec machine creds missing CrowdSec 那一格會是 no_machine_creds,決策變 partial。重跑 --reconfigure 會重新註冊機器身分
PATCH <sc> → 409 回報被主機 A 拒絕:這筆已經被別的執行端拾起,或已經是終態。單一節點很少見;見到通常是同一筆被處理了兩次
edl prune dropped N expired 正常。EDL 清單每分鐘剔除過期項目,有剔到才印

od-bridge 在整體裡的位置到此說完。它是整個安裝包裡最小的一個程式,也是唯一一個「兩台主機之間」的程式:事件經它出去,決策經它回來,而它對兩者都不做判斷。讀者把這套東西拆開來改的時候,Vector 可以換成 Fluent Bit、CrowdSec 可以不裝、Grafana 可以不要,但這一千行要留著,或者照第 3 篇列的契約自己寫一支。契約只有三個端點、兩套身分、六個欄位。搭配元件篇與專篇到這裡是九篇;安裝包裡剩下的主機層腳本(tunnel 建立、網卡卸載、紀錄輪替、歡迎頁),合在最後一篇講。


上一篇
Vector:三種告警變成一種事件
系列文
一鍵完成六套開源防禦系統整合 共 28 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言